// Copyright 2010, Vanya Davidenko.
// Используемая кодировка: UTF-8.

#ifndef SCRW_MANAGER_H_
#define SCRW_MANAGER_H_

#include <deque>
#include <queue>
#include <sstream>
#include <libv/lock.h>
#include <libv/lock_spin.h>
#include <unicode/ustream.h>
#include <unicode/regex.h>

#include "curl.h"
#include "common.h"
#include "url_converter.h"



namespace scrw
{

/* Задачей менеджера является создание заданий на основе полученных ссылок,
 * а также выдача результата: текста загруженных страниц. Для исключения
 * создания одинаковых заданий все просмотренные ссылки сохраняются.
 * При возврате ошибки одним из модулей менеджер выводит соответствующее
 * сообщение.
 * Сценарий работы
 *  1.	Менеджер начинает работу с получения ссылки на первую страницу.
 *  2.	Он добавляет ее в приоритетную очередь заданий модуля загрузки страниц.
 *  3.	Модуль загрузки страниц, получив задание, загружает ее и передает
 *      менеджеру.
 *  4.	Полученная страница добавляется менеджером в очередь заданий модуля
 *      выделения текста и ссылок.
 *  5.	Модуль выделения текста ссылок, получив задание, последовательно
 *      выделяет ссылки и текст и возвращает их менеджеру.
 *  6.	Полученный менеджером текст заносится в список результатов. Полученные
 *      ссылки проходят фильтрацию: последовательно проверяются двумя
 *      регулярными выражениями, передаваемыми пользователем. Неудовлетворение
 *      первому регулярному выражению или удовлетворение второму исключает
 *      ссылку из дальнейшей обработки.
 *  7.	Краулер завершает свою работу в случае, если не осталось больше никаких
 *      заданий и в момент проверки ни одно из них не выполняется. */
class Manager : libv::Uncopyable
{
  public:
    class Downloader;
    class Extractor;

    /** Загруженная страница. */
    struct Page
    {
        /** Конструктор по умолчанию. Используется стандартными контейнерами. */
        Page() {}

        Page(const UnicodeUrl& url_,
             const UnicodeUrl& parent_url_,
             const icu::UnicodeString& text_,
             const ::std::vector<char> data_,
             const MimeType& mime_type_)
            : url(url_),
              parent_url(parent_url_),
              text(text_),
              data(data_),
              mime_type(mime_type_) {}

        /** Адрес страницы. */
        UnicodeUrl url;

        /** Ссылка на страницу, которая ссылалась на url. */
        UnicodeUrl parent_url;

        /** Выделенный текст в юникоде. */
        icu::UnicodeString text;

        /** Содержимое оригинальной страницы. */
        ::std::vector<char> data;

        /** Mime-тип страницы. */
        MimeType mime_type;
    };


    /** Статистика о работе класса. */
    struct Stats
    {
        Stats()
            : num_pages_to_download(0),
              num_pages_to_extract(0),
              num_pages(0) {}

        Stats(const size_t num_pages_to_download_,
              const size_t num_pages_to_parse_,
              const size_t num_pages_)
            : num_pages_to_download(num_pages_to_download_),
              num_pages_to_extract(num_pages_to_parse_),
              num_pages(num_pages_) {}

        /** Число страниц, поставленных в очередь на загрузку. */
        size_t num_pages_to_download;

        /** Число страниц, поставленных в очередь на выделение текста. */
        size_t num_pages_to_extract;

        /** Число готовых страниц, которые можно забрать через PopPage(). */
        size_t num_pages;
    };


    /** Конструктор.
      * @pre url и регулярные выражения должны быть корректными.
      * @param[in] include_matcher_string регулярное выражение, определяющее
      *            подлежит ли страница загрузке (первый фильтр),
      *            значение по умолчанию: "^.*$"
      * @param[in] exclude_matcher_string регулярное выражение, определяющее
      *            подлежит ли страница, подошедшая под include_matcher_string
      *            пропуску (второй фильтр), значение по умолчанию: "^$"
      * @param[in] urls_hash_table_size размер хэш-таблицы, в которой
      *            сохраняются посещенные URL.
      *            Рекомендуется использовать простое число.
      * @exception libv::PrecondException в случае, если переданные регулярные
      * выражения не являются корректными. */
    Manager(const icu::UnicodeString& include_matcher_string
                = icu::UnicodeString("^.*$"),
            const icu::UnicodeString& exclude_matcher_string
                = icu::UnicodeString("^$"),
            const icu::UnicodeString& yield_matcher_string
                = icu::UnicodeString("^.*$"),
            const size_t urls_hash_table_size = 524287)  // простое число
        : grab_nofollow_(false),
          err_(U_ZERO_ERROR),
          include_matcher_(include_matcher_string, 0, err_),
          exclude_matcher_(exclude_matcher_string, 0, err_),
          yield_matcher_(yield_matcher_string, 0, err_),
          num_pages_downloading_(0),
          num_pages_extracting_(0)
    {
        PRECOND(U_SUCCESS(err_) == TRUE);  // создание регуляров
        PRECOND(include_matcher_string.isBogus() == FALSE);
        PRECOND(exclude_matcher_string.isBogus() == FALSE);
        PRECOND(urls_hash_table_size > 0);

        set_connect_timeout(30);
        set_timeout(30);
        set_cookie("");
        set_user_agent("scrw (based on libcurl)");
        set_referer("");
        set_low_speed_limit(5000, 10);
        set_grab_nofollow(false);
        set_extract_text(true);
        set_max_num_redirects(10);

        // Создаем все нулевые указатели здесь, так как в случае генерации
        // исключения libv::PrecondException выше производится освобождение
        // urls_hash_table_size нулевых указателей, что вызывает
        // ощутимую задержку тогда, когда создание объекта должно завершиться.
        known_urls_.assign(urls_hash_table_size, NULL);
    }


    ~Manager()
    {
        assert(num_pages_downloading_ == 0);
        assert(num_pages_extracting_ == 0);

        for ( ::std::deque< ::std::list<UnicodeUrl>* >::iterator\
              i = known_urls_.begin() ; i != known_urls_.end() ; ++i )
            delete *i;
    }


    /** Конвертирование UAsciiUrl в UnicodeUrl.
      * @exception UrlConverterException.
      * Метод потокобезопасен. */
    UnicodeUrl ConvertAsiiUrl2UnicodeUrl(const UAsciiUrl& url) const
    {
        libv::Lock dnd(&url_converter_lock_);
        return url_converter_.Ascii2Unicode(url);
    }


    /* Добавление начального адреса и глубина обхода по нему.
     * @param[in] url адрес документа, с которого начинается загрузка
     * @param[in] max_depth максимальная глубина загрузки (0 — загрузить только
     *            начальную страницу, 1 — загрузить начальную страницу и те,
     *            на которые она ссылается и т.д.)
     * @exception UrlConverterException если url не является корректным адресом.
     * Метод потокобезопасен. */
    void AddUrl(const UAsciiUrl& url, const int32_t max_depth)
    {
        PRECOND(url.isBogus() == FALSE);
        PRECOND(max_depth >= 0);

        const UnicodeUrl u_url = ConvertAsiiUrl2UnicodeUrl(url);

        if ( AddUrlIfUnknown(u_url) )
        {
            LOG("Adding new url."
                << "\n    url: " << OutputUrl(u_url)
                << "\n    max depth: " << max_depth);

            libv::Lock dnd(&downloader_jobs_lock_);
            downloader_jobs_.push(
                DownloadJob(u_url, UnicodeUrl(""), 0, max_depth));
            dnd.Unlock();
        }
        else
        {
            LOG("Skipping known url"
                << "\n    url: " << OutputUrl(u_url)
                << "\n    max depth: " << max_depth);
        }
    }


    /** Задание адреса прокси в формате host[:port].
      * Значение по умолчанию: "".
      * Метод потокобезопасен и не генерирует исключения. */
    Manager& set_proxy(const ::std::string& proxy)
    {
        libv::Lock dnd(&settings_lock_);
        curl_.set_default_proxy(proxy);
        return *this;
    }

    /** Задание величины времени ожидания соединения с удаленным сервером
      * в секундах для всех созданных в дальнейшем объектов класса Downloader.
      * Значение по умолчанию: 5.
      * Метод потокобезопасен и не генерирует исключения. */
    Manager& set_connect_timeout(const size_t timeout)
    {
        libv::Lock dnd(&settings_lock_);
        curl_.set_default_connect_timeout(timeout);
        return *this;
    }


    /** Задание величины максимального времени загрузки одной страницы
      * в секундах для всех созданных в дальнейшем объектов класса Downloader.
      * Значение по умолчанию: 15.
      * Метод потокобезопасен и не генерирует исключения. */
    Manager& set_timeout(const size_t timeout)
    {
        libv::Lock dnd(&settings_lock_);
        curl_.set_default_timeout(timeout);
        return *this;
    }


    /** Задание значения cookie в формате "name1=var1;name2=var2;" для всех
      * созданных в дальнейшем объектов класса Downloader.
      * Повторный вызов перепишет предыдущее значение.
      * Проверка корректности происходит при первом использовании.
      * Значение по умолчанию: пустая строка.
      * Метод потокобезопасен и не генерирует исключения. */
    Manager& set_cookie(const ::std::string& cookie)
    {
        libv::Lock dnd(&settings_lock_);
        curl_.set_default_cookie(cookie);
        return *this;
    }


    /** Задание строки, передаваемой в качестве User Agent, для всех
      * созданных в дальнейшем объектов класса Downloader.
      * Эта настройка позволяет "обмануть" удаленный сервер.
      * Повторный вызов перепишет предыдущее значение.
      * Проверка корректности происходит при первом использовании.
      * Значение по умолчанию: "scrw 1.0 (based on libcurl)".
      * Метод потокобезопасен и не генерирует исключения. */
    Manager& set_user_agent(const ::std::string& user_agent)
    {
        libv::Lock dnd(&settings_lock_);
        curl_.set_default_user_agent(user_agent);
        return *this;
    }


    /** Задание строки, передаваемой в качестве Referer, для всех
      * созданных в дальнейшем объектов класса Downloader.
      * Эта настройка позволяет "обмануть" удаленный сервер.
      * Повторный вызов перепишет предыдущее значение.
      * Проверка корректности происходит при первом использовании.
      * Значение по умолчанию: пустая строка.
      * Метод потокобезопасен и не генерирует исключения. */
    Manager& set_referer(const ::std::string& referer)
    {
        libv::Lock dnd(&settings_lock_);
        curl_.set_default_referer(referer);
        return *this;
    }


    /** Задание минимальной скорости загрузки страницы для всех создаваемых
      * в дальнейшем объектов класса Downloader.
      * Загрузка завершится в случае, если скорость соединения с ней
      * в течение duration_s секунд не превысит скорости speed б/с.
      * Повторный вызов перепишет предыдущее значение.
      * Проверка корректности происходит при первом использовании.
      * Значения по умолчанию: 5 кб/с в течении 5 секунд.
      * Метод потокобезопасен и не генерирует исключения. */
    Manager& set_low_speed_limit(const size_t speed, const size_t duration_s)
    {
        libv::Lock dnd(&settings_lock_);
        curl_.set_default_low_speed_limit(speed, duration_s);
        return *this;
    }


    /** Задание флага, определяющего добавлять ли ссылки с аттрибутом
      * rel="nofollow" в очередь заданий, для всех создаваемых в дальнейшем
      * объектов класса Extractor.
      * Значение по умолчанию: "false". */
    Manager& set_grab_nofollow(const bool grab_nofollow)
    {
        libv::Lock dnd(&settings_lock_);
        grab_nofollow_ = grab_nofollow;
        return *this;
    }


    /** Задание флага, определяющего выделять ли из страниц только ссылки 
      * (false) или ссылки и текст (false) для всех создаваемых в дальнейшем 
      * объектов класса Extractor.
      * Значение по умолчанию: "true". */
    Manager& set_extract_text(const bool extract_text)
    {
        libv::Lock dnd(&settings_lock_);
        extract_text_ = extract_text;
        return *this;
    }


    /** Задание максимального числа переходов (редиректов).
      * Значение 0 запрещает переходы.
      * Повторный вызов перепишет предыдущее значение.
      * Проверка корректности происходит при первом использовании.
      * Значение по умолчанию: 10.
      * Метод потокобезопасен и не генерирует исключения. */
    Manager& set_max_num_redirects(const size_t num_redirects)
    {
        libv::Lock dnd(&settings_lock_);
        curl_.set_default_max_num_redirects(num_redirects);
        return *this;
    }


    /** Создание нового объекта класса Downloader.
      * @sa Manager::Downloader.
      * Метод потокобезопасен и не генерирует исключения. */
    Downloader GetDownloader();


    /** Создание нового объекта класса Extractor.
      * Присваивание данному объекту запрещено, так как он агрегирует
      * объекты с таким же ограничением. Тем не менее, разрешено копирование.
      * Таким образом, возможно использование объекта через указатель на него:
      * <code>
      * Manager::Extractor* e = new Manager::Extractor(m.GetExtractor());
      * // ...
      * delete e;
      * </code>
      * Метод потокобезопасен и не генерирует исключения. */
    Extractor GetExtractor();


    /** Получение очередной загруженной страницы.
      * В случае, если очередь страниц пуста возвращается страница
      * созданная конструктором по умолчанию. Данное решение является
      * единственно приемлемым при многопоточном использовании данного метода.
      * Метод потокобезопасен. */
    Page PopPage()
    {
        libv::Lock dnd(&pages_lock_);
        if ( pages_.empty() )
            return Page();

        const Page result = pages_.front();
        pages_.pop();
        return result;
    }


    /** Получение статистики (объекта класса Stats) о работе менеджера.
      * Метод потокобезопасен и не генерирует исключения. */
    Stats Statistics() const
    {
        libv::Lock dnd1(&downloader_jobs_lock_);
        libv::Lock dnd2(&extractor_jobs_lock_);
        libv::Lock dnd3(&pages_lock_);

        // Все значения в один момент времени за счет одновременной блокировки.
        const size_t num_pages_to_download = downloader_jobs_.size();
        const size_t num_pages_to_extract = extractor_jobs_.size();
        const size_t num_pages = pages_.size();

        dnd3.Unlock();
        dnd2.Unlock();
        dnd1.Unlock();

        return Stats(num_pages_to_download, num_pages_to_extract, num_pages);
    }


    /** Получение числа страниц, доступных к выдаче.
      * Метод выполняется быстрее, чем Statistics().
      * Метод потокобезопасен и не генерирует исключения. */
    size_t num_pages() const
    {
        libv::Lock dnd(&pages_lock_);
        return pages_.size();
    }


    /** Метод, возвращающий true после выполнения всех задач.
      * Метод потокобезопасен и не генерирует исключения. */
    bool IsAllDone() const
    {
        // Чаще всего выход будет сразу здесь,
        // так как загрузка — самая медленная операция.
        libv::Lock dnd1(&downloader_jobs_lock_);
        if ( !downloader_jobs_.empty() )
            return false;

        // Важен именно такой порядок — иначе, deadlock!
        libv::Lock dnd2(&extractor_jobs_lock_);
        libv::Lock dnd3(&pages_lock_);
        libv::Lock dnd4(&num_pages_downloading_lock_);
        libv::Lock dnd5(&num_pages_extracting_lock_);

        // Из-за данного метода необходимо, что бы все переменные имели
        // внешние блокировки: возможно использование атомарных операций
        // над num_pages_downloading_ и num_pages_extracting_.
        // Это сработало бы везде, кроме данной проверки, в которой требуется
        // чтобы данные значения были равны нулю в *один* момент времени,
        // но добиться этого можно только с использованием внешних блокировок.

        return downloader_jobs_.empty() &&
               extractor_jobs_.empty() &&
               pages_.empty() &&
               num_pages_downloading_ == 0 &&
               num_pages_extracting_ == 0;
    }


  private:
    Manager& operator=(const Manager&);

    /** Задание на загрузку страницы.
      * Признак пустого задания: depth == -1. */
    struct DownloadJob
    {
        /** Конструктор по умолчанию. Создает пустое задание.
          * Возвращается PopDownloadJob() при пустой очереди заданий. */
        DownloadJob() : depth(-1) {}

        /** @pre url.isBogus() == FALSE
          * @pre parent_url_.isBogus() == FALSE
          * @pre depth_ >= 0.
          * @exception libv::PrecondException. */
        DownloadJob(const UnicodeUrl& url_,
                    const UnicodeUrl& parent_url_,
                    const int32_t depth_,
                    const int32_t max_depth_)
            : url(url_),
              parent_url(parent_url_),
              depth(depth_),
              max_depth(max_depth_)
        {
            PRECOND(url.isBogus() == FALSE);
            PRECOND(parent_url_.isBogus() == FALSE);
            PRECOND(depth_ >= 0 && depth_ <= max_depth_);
            PRECOND(max_depth_ >= 0);
        }


        /** Адрес страницы, которую необходимо загрузить в Unicode-формате.
          * @warning Данное значение не должно изменяться. */
        /*const*/ UnicodeUrl url;

        /** Ссылка на страницу, которая ссылалась на url.
          * @warning Данное значение не должно изменяться. */
        /*const*/ UnicodeUrl parent_url;

        /** Глубина ссылки. Значение изменяется от 0 до max_depth_,
          * задаваемых в конструкторе.
          * @warning Данное значение не должно изменяться. */
        /*const*/ int32_t depth;

        /** Максимальная глубина загрузки данной "ветки".
          * @warning Данное значение не должно изменяться. */
        /*const*/ int32_t max_depth;

        /** Буфер, в который будут записаны полученные данные. */
        ::std::vector<char> data;

        /** Буфер, в который по возможности будет записан Mime-тип данных. */
        MimeType mime_type;

        /** Буфер, в который по возможности будет записана кодировка. */
        Charset charset;
    };


    /** Задание на обработку страницы (выделение текста и ссылок).
      * Признак пустого задания: depth == -1. */
    struct ExtractorJob
    {
        /** Конструктор по умолчанию. Создает пустое задание.
          * Возвращается PopExtractorJob при пустой очереди заданий. */
        ExtractorJob()
            : depth(-1) {}

        /** @pre url.isBogus() == FALSE
          * @pre parent_url_.isBogus() == FALSE
          * @pre depth_ >= 0.
          * @exception libv::PrecondException. */
        ExtractorJob(const UnicodeUrl& url_,
                     const UnicodeUrl& parent_url_,
                     const int32_t depth_,
                     const int32_t max_depth_,
                     const ::std::vector<char>& data_,
                     const MimeType& mime_type_,
                     const Charset& charset_)
            : url(url_),
              parent_url(parent_url_),
              depth(depth_),
              max_depth(max_depth_),
              data(data_),
              mime_type(mime_type_),
              charset(charset_)
        {
            PRECOND(url.isBogus() == FALSE);
            PRECOND(parent_url_.isBogus() == FALSE);
            PRECOND(depth_ >= 0 && depth_ <= max_depth_);
            PRECOND(max_depth_ >= 0);
        }


        /** Адрес обрабатываемой страницы в Unicode-формате.
          * @warning Данное значение не должно изменяться. */
        /*const*/ UnicodeUrl url;

        /** Ссылка на страницу, которая ссылалась на url.
          * @warning Данное значение не должно изменяться. */
        /*const*/ UnicodeUrl parent_url;

        /** Глубина ссылки. Значение изменяется от max_depth, задаваемого в
          * PushURL, до 0, являющегося признаком остановки загрузки.
          * @warning Данное значение не должно изменяться. */
        /*const*/ int32_t depth;

        /** Максимальная глубина загрузки данной "ветки".
          * @warning Данное значение не должно изменяться. */
        /*const*/ int32_t max_depth;

        /** Данные со страницы в кодировке charset.
          * @warning Данное значение не должно изменяться. */
        /*const*/ ::std::vector<char> data;

        /** Mime-тип страницы. */
        /*const*/ MimeType mime_type;

        /** Кодировка страницы.
          * @warning Данное значение не должно изменяться. */
        /*const*/ Charset charset;


        /** Буфер, в который будут записаны выделенные ссылки. */
        ::std::deque<UnicodeUrl> urls;

        /** Буфер, в который будет записан выделенный текст. */
        icu::UnicodeString text;
    };


    /** Функтор "меньше" для приоритетов.
      * Для обхода в ширину приоритет тем выше, чем ниже текущая глубина. */
    template<class TJob>
    struct PriorityLess
    {
        typedef const TJob& first_argument_type;
        typedef const TJob& second_argument_type;
        typedef bool result_type;

        bool operator()(const TJob& lhv, const TJob& rhv) const
        {
            return lhv.depth > rhv.depth;
        }
    };


    typedef ::std::priority_queue<DownloadJob,
                                  ::std::deque<DownloadJob>,
                                  PriorityLess<DownloadJob> > DownloaderQueue;

    typedef ::std::priority_queue<ExtractorJob,
                                  ::std::deque<ExtractorJob>,
                                  PriorityLess<ExtractorJob> > ExtractorQueue;

    typedef ::std::queue<Page> PageQueue;


    /** Получение нового задания загрузки данных.
      * В случае, если очередь заданий пуста, метод возвращает пустое.
      * Метод потокобезопасен и не генерирует исключения.
      * @warning каждому вызову метода должен соответствовать один из вызовов:
      *          PushDownloadJobResult() или PushFailedDownloadJob(). */
    DownloadJob PopDownloadJob()
    {
        libv::Lock dnd(&downloader_jobs_lock_);
        if ( downloader_jobs_.empty() )
        {
            dnd.Unlock();
            return DownloadJob();
        }

        // Необходимо, что бы не было момента, когда очередь задач уже пуста,
        // а num_pages_downloading_ еще не инкрементирован,
        // иначе AllDone() вернет ложный true.
        libv::Lock dnd2(&num_pages_downloading_lock_);
        num_pages_downloading_++;
        dnd2.Unlock();

        const DownloadJob result = downloader_jobs_.top();
        downloader_jobs_.pop();
        dnd.Unlock();

        LOG("Downloader got new job."
            << "\n    url: " << OutputUrl(result.url)
            << "\n    parent url: " << OutputUrl(result.parent_url)
            << "\n    depth: " << result.depth
            << "\n    max depth: " << result.max_depth);

        return result;
    }


    /** Возврат успешно выполненного задания загрузки данных.
      * Метод потокобезопасен и не генерирует исключения. */
    void PushDownloadJobResult(const DownloadJob& result)
    {
        if ( result.data.empty() )
        {
            LOG("Downloader finished job."
                << "\n    url: " << OutputUrl(result.url)
                << "\n    parent url: " << OutputUrl(result.parent_url)
                << "\n    depth: " << result.depth
                << "\n    max depth: " << result.max_depth
                << "\n    mime type: " << result.mime_type
                << "\n    charset: " << result.charset
                << "\n    It's an empty file, skipping.");
        }
        else
        {
            LOG("Downloader finished job."
                << "\n    url: " << OutputUrl(result.url)
                << "\n    parent url: " << OutputUrl(result.parent_url)
                << "\n    depth: " << result.depth
                << "\n    max depth: " << result.max_depth
                << "\n    mime type: " << result.mime_type
                << "\n    charset: " << result.charset
                << "\n    size: " << result.data.size());

            // Уменьшаем время блокировки, отдельно создавая задачу.
            try
            {
                const ExtractorJob job(result.url,
                                       result.parent_url,
                                       result.depth,
                                       result.max_depth,
                                       result.data,
                                       result.mime_type,
                                       result.charset);

                libv::Lock dnd(&extractor_jobs_lock_);
                extractor_jobs_.push(job);
                dnd.Unlock();
            }
            catch(libv::PrecondException& e)
            {
                // Данное catch скорее всего никогда не сработает, но важно
                // поймать исключение, что бы num_pages_downloading_ точно
                // декрементировался.
                ERR("ExtractorJob constructor precondition failed: "
                    << e.what());
            }
        }

        libv::Lock dnd2(&num_pages_downloading_lock_);
        assert(num_pages_downloading_ > 0);
        num_pages_downloading_--;
        dnd2.Unlock();
    }


    /** Возврат прерванного задания и поясняющего сообщения.
      * Метод потокобезопасен и не генерирует исключения. */
    void PushAbortedDownloadJob(const DownloadJob& job,
                                const ::std::string& msg)
    {
        LOG("Download aborted."
            << "\n    url: " << OutputUrl(job.url)
            << "\n    parent url: " << OutputUrl(job.parent_url)
            << "\n    depth: " << job.depth
            << "\n    max depth: " << job.max_depth
            << "\n    message: " << msg);

        libv::Lock dnd(&num_pages_downloading_lock_);
        assert(num_pages_downloading_ > 0);
        num_pages_downloading_--;
        dnd.Unlock();
    }



    /** Возврат невыполненного задания и сообщения об ошибке.
      * Метод потокобезопасен и не генерирует исключения. */
    void PushFailedDownloadJob(const DownloadJob& job, const ::std::string& msg)
    {
        ERR("Download failed."
            << "\n    url: " << OutputUrl(job.url)
            << "\n    parent url: " << OutputUrl(job.parent_url)
            << "\n    depth: " << job.depth
            << "\n    max depth: " << job.max_depth
            << "\n    message: " << msg);

        libv::Lock dnd(&num_pages_downloading_lock_);
        assert(num_pages_downloading_ > 0);
        num_pages_downloading_--;
        dnd.Unlock();
    }



    /** Получение нового задания выделения данных из файла.
      * В случае, если очередь заданий пуста, метод возвращает пустое.
      * Метод потокобезопасен и не генерирует исключения.
      * @warning каждому вызову метода должен соответствовать один из вызовов:
      *          PushExtractorJobResult() или PushFailedExtractorJob(). */
    ExtractorJob PopExtractorJob()
    {
        libv::Lock dnd(&extractor_jobs_lock_);
        if ( extractor_jobs_.empty() )
        {
            dnd.Unlock();
            return ExtractorJob();
        }

        libv::Lock dnd2(&num_pages_extracting_lock_);
        num_pages_extracting_++;
        dnd2.Unlock();

        const ExtractorJob result = extractor_jobs_.top();
        extractor_jobs_.pop();
        dnd.Unlock();

        LOG("Extractor got new job."
            << "\n    url: " << OutputUrl(result.url)
            << "\n    parent url: " << OutputUrl(result.parent_url)
            << "\n    depth: " << result.depth
            << "\n    max depth: " << result.max_depth
            << "\n    mime type: " << result.mime_type
            << "\n    charset: " << result.charset);
        return result;
    }


    /** Метод возвращает true, если задание job использовалось для обработки
      * rss/atom и false в противном случае.
      * Метод потокобезопасен и не генерирует исключения. */
    bool IsRss(const ExtractorJob& job) const
    {
        return job.mime_type.find("xhtml") == MimeType::npos &&
               job.mime_type.find("xml") != MimeType::npos;
    }


    /** Потокобезопасная проверка того, что URL подходит под include_matcher_.*/
    bool PassedIncludeMatcher(const UnicodeUrl& url)
    {
        libv::Lock dnd(&include_matcher_lock_);
        include_matcher_.reset(url);

        err_ = U_ZERO_ERROR;
        const bool result = include_matcher_.matches(err_) == TRUE;
        VERIFY(U_SUCCESS(err_) == TRUE);

        return result;
    }


    /** Потокобезопасная проверка того, что URL подходит под exclude_matcher_.*/
    bool PassedExcludeMatcher(const UnicodeUrl& url)
    {
        libv::Lock dnd(&exclude_matcher_lock_);
        exclude_matcher_.reset(url);

        err_ = U_ZERO_ERROR;
        const bool result = exclude_matcher_.matches(err_) == TRUE;
        VERIFY(U_SUCCESS(err_) == TRUE);

        return result;
    }


    bool PassedYieldMatcher(const UnicodeUrl& url)
    {
        libv::Lock dnd(&yield_matcher_lock_);
        yield_matcher_.reset(url);

        err_ = U_ZERO_ERROR;
        const bool result = yield_matcher_.matches(err_) == TRUE;
        VERIFY(U_SUCCESS(err_) == TRUE);

        return result;
    }


    /** Добавление URL в хэш таблицу известных адресов.
      * В случае, если такого адреса не было в таблице — возвращается true,
      * в противном случае — false.
      * Метод потокобезопасен и не генерирует исключения. */
    bool AddUrlIfUnknown(const UnicodeUrl& url)
    {
        // SDBM hash
        size_t hash = 0;
        for ( int32_t i = 0 ; i != url.length() ; ++i )
            hash = (size_t)url[i] + (hash << 6) + (hash << 16) - hash;
        const size_t index = hash % known_urls_.size();

        // Хэширование — вне блокировки.
        libv::Lock dnd(&known_urls_lock_);

        ::std::list<UnicodeUrl>* chain = known_urls_[index];
        if ( chain == NULL )
        {
            known_urls_[index] = new ::std::list<UnicodeUrl>();
            known_urls_[index]->push_back(url);
            return true;
        }
        else if ( ::std::find(chain->begin(),chain->end(),url) == chain->end() )
        {
            chain->push_back(url);
            return true;
        }

        return false;
    }



    /** Возврат успешно выполненного задания по выделению данных из файла.
      * Метод потокобезопасен и не генерирует исключения. */
    void PushExtractorJobResult(const ExtractorJob& result)
    {
        LOG("Extractor finished job."
            << "\n    url: " << OutputUrl(result.url)
            << "\n    parent url: " << OutputUrl(result.parent_url)
            << "\n    depth: " << result.depth
            << "\n    max depth: " << result.max_depth
            << "\n    mime type: " << result.mime_type
            << "\n    charset: " << result.charset);

        if ( !IsRss(result) )
        {
            LOG("\n    Adding to pages queue.");
            PushPage(result.url, result.parent_url, 
                     result.text, result.data, result.mime_type);
        }
        else if (IsRss(result))
        {
            LOG("\n    No pages added to queue, RSS contains only links.");
        }
        else
        {
            LOG("\n    No pages added to queue,"
                "url didn't match yield pattern.");
        }


        if ( result.depth == result.max_depth )
        {
            LOG("\n    Max depth reached, stopping.");
        }
        else if ( result.urls.empty() )
        {
            LOG("\n    There is no urls on page.");
        }
        else
        {
            for ( ::std::deque<UnicodeUrl>::const_iterator \
                  i = result.urls.begin() ; i != result.urls.end() ; ++i )
            {
                MSG("\n    Extracted url: " << OutputUrl(*i));

                if ( PassedIncludeMatcher(*i) == false )
                {
                    MSG("\n        Didn't match include pattern.");
                    continue;
                }

                if ( PassedExcludeMatcher(*i) )
                {
                    MSG("\n        Matched exclude pattern.");
                    continue;
                }

                if ( AddUrlIfUnknown(*i) == false )
                {
                    MSG("\n        Skipping, already known.");
                    continue;
                }

                assert(!i->isBogus() && !result.url.isBogus());


                // Оптимизация: если краулер работает в режиме выгрузки ссылок
                // (без текста), тогда нет необходимости посещать листовые
                // страницы: их ссылку мы знаем, а это единственное, что нам
                // требуется. Так что отдаем фиктивную страницу (объект Page)
                // только с ссылкой.
                /*
                if ( !extract_text_ && result.depth + 1 == result.max_depth )
                {
                    MSG("\n        This page is a leaf, so pushing its urls as "
                        "a null-page.");

                    PushPage(*i, result.url,
                             icu::UnicodeString(), ::std::vector<char>(), "");
                    continue;
                }
                
                */

                try
                {
                    DownloadJob job;
                    if ( IsRss(result) )
                    {
                        MSG("\n        Making new rss download job");
                        const int32_t d =
                            result.depth + 1 == result.max_depth ? 1 : 0;
                        job = DownloadJob(*i, result.url, d, 1);
                    }
                    else
                    {
                        LOG("\n        Making new download job");
                        job = DownloadJob(*i, result.url,
                                          result.depth + 1, result.max_depth);
                    }

                    libv::Lock dnd4(&downloader_jobs_lock_);
                    downloader_jobs_.push(job);
                    dnd4.Unlock();
                }
                catch(libv::PrecondException& e)
                {
                    // Данное catch скорее всего никогда не сработает, но важно
                    // поймать исключение, что бы num_pages_extracting_ точно
                    // декрементировался.
                    ERR("DownloaderJob constructor precondition failed: "
                        << e.what());
                }
            }
        }

        libv::Lock dnd5(&num_pages_extracting_lock_);
        assert(num_pages_extracting_ > 0);
        num_pages_extracting_--;
        dnd5.Unlock();
    }


    /** Возврат невыполненного задания и сообщения об ошибке.
      * Метод потокобезопасен и не генерирует исключения. */
    void PushFailedExtractorJob(const ExtractorJob& job,
                                const ::std::string& msg)
    {
        ERR("Extraction failed."
            << "\n    url: " << OutputUrl(job.url)
            << "\n    parent url: " << OutputUrl(job.parent_url)
            << "\n    depth: " << job.depth
            << "\n    max depth: " << job.max_depth
            << "\n    mime type: " << job.mime_type
            << "\n    charset: " << job.charset
            << "\n    message: " << msg);

        libv::Lock dnd(&num_pages_extracting_lock_);
        assert(num_pages_extracting_ > 0);
        num_pages_extracting_--;
        dnd.Unlock();
    }


    void PushPage(const UnicodeUrl& url, 
                  const UnicodeUrl& parent_url,
                  const icu::UnicodeString& text,
                  const ::std::vector<char>& data,
                  const MimeType& mime_type)
    {
        if ( PassedYieldMatcher(url) )
        {
            const Page page(url, parent_url, text, data, mime_type);

            libv::Lock dnd1(&pages_lock_);
            pages_.push(page);
            dnd1.Unlock();
        }
    }


    /** Получения сообщения о ситуации, возникшей при выполнении задания job. */
    void SendMessage(const ExtractorJob& job, const ::std::string& msg)
    {
        LOG(msg
            << "\n    url: " << OutputUrl(job.url)
            << "\n    parent url: " << OutputUrl(job.parent_url)
            << "\n    depth: " << job.depth
            << "\n    max depth: " << job.max_depth
            << "\n    mime type: " << job.mime_type
            << "\n    charset: " << job.charset
            << "\n    message: " << msg);
    }



    ::std::string OutputUrl(const UnicodeUrl& url) const
    {
#ifdef _WIN32  // кодировка консоли (866) отличается от системной (1251).
        return UnicodeString2String(url, "cp866");
#else
        return UnicodeString2String(url, NULL);
#endif
    }


    /** Общий для всех загрузчиков объект, из которого они получают хэндл. */
    Curl curl_;

    /** Флаг передаваемый в конструктор Extractor. */
    bool grab_nofollow_;

    /** Выгружать ссылки и текст (true) или только ссылки (false). */
    bool extract_text_;

    /** Блокировка объекта curl_ и grab_nofollow_. */
    libv::Spin settings_lock_;


    /** Переменная, используемая для получения кода ошибки функций ICU. */
    mutable UErrorCode err_;


    mutable UrlConverter url_converter_;

    /** Блокировка объекта url_converter_ */
    mutable libv::Spin url_converter_lock_;


    /** Регулярное выражение, определяющее принадлежит ли URL к шаблону
      * загружаемых адресов. */
    icu::RegexMatcher include_matcher_;

    /** Блокировка объекта include_matcher_. */
    mutable libv::Spin include_matcher_lock_;


    /** Регулярное выражение, определяющее принадлежит ли загружаемый URL
      * к шаблону пропускаемых URL (выполняется после include_matcher_). */
    icu::RegexMatcher exclude_matcher_;

    /** Блокировка объекта exclude_matcher_. */
    mutable libv::Spin exclude_matcher_lock_;


    /** Регулярное выражение, определяющее принадлежит ли загружаемый URL
      * к шаблону страниц, подлежащих выдаче в результат
      * (выполняется после include_matcher_). */
    icu::RegexMatcher yield_matcher_;

    /** Блокировка объекта yield_matcher_. */
    mutable libv::Spin yield_matcher_lock_;



    /** Задания на загрузку страниц. */
    DownloaderQueue downloader_jobs_;

    /** Блокировка объекта downloader_jobs_. */
    mutable libv::Spin downloader_jobs_lock_;


    /** Число загрузок, выполняемых в данный момент. */
    size_t num_pages_downloading_;

    /** Блокировка объекта downloader_jobs_. */
    mutable libv::Spin num_pages_downloading_lock_;


    /** Задание на обработку страниц (выделение текста и url). */
    ExtractorQueue extractor_jobs_;

    /** Блокировка объекта extractor_jobs_. */
    mutable libv::Spin extractor_jobs_lock_;


    /** Число операций выделения текста, выполняемых в данный момент. */
    size_t num_pages_extracting_;

    /** Блокировка объекта num_pages_extracting_. */
    mutable libv::Spin num_pages_extracting_lock_;


    /** Коллекция посещенных адресов. */
    ::std::deque< ::std::list<UnicodeUrl>* > known_urls_;

    /** Блокировка объекта visited_urls_. */
    mutable libv::Spin known_urls_lock_;


    /** Готовые страницы (url и текст), возвращаемые пользователю. */
    PageQueue pages_;

    /** Блокировка pages_. */
    mutable libv::Spin pages_lock_;

};

}  // scrw


#include "downloader.hpp"
#include "extractor.hpp"


namespace scrw
{

Manager::Downloader Manager::GetDownloader()
{
    libv::Lock dnd(&settings_lock_);
    return Downloader(this, curl_.GetHandle());
}

Manager::Extractor Manager::GetExtractor()
{
    libv::Lock dnd(&settings_lock_);
    return Extractor(this, grab_nofollow_, extract_text_);
}

}  // scrw

#endif  // SCRW_MANAGER_H_
